<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WTS日志</title>
    <style>
        :root {
            --bg-color: #1e1e1e;
            --text-color: #e0e0e0;
            --font-family: 'Courier New', monospace;
            --prompt-color: #4e9a06;
        }

        body {
            margin: 0;
            padding: 0;
            background-color: var(--bg-color);
            color: var(--text-color);
            font-family: var(--font-family);
            line-height: 1.4;
        }

        .terminal-controls {
            position: fixed;
            top: -1px;
            right: 15px;
            padding: 5px;
            z-index: 100;
        }

        .terminal-controls button{
            background-color: #285a63;
            border: none;
            border-radius: 12px;
            color:white;
            padding: 5px 14px 6px 14px;
            text-align: center;
            text-decoration: none;
            display: inline-block;
            font-size: 11px;
            margin: 4px 2px;
            cursor: pointer;
            transition-duration: 0.4s;
            -webkit-transition-duration: 0.4s;
        }

        .terminal-controls button:hover{
            box-shadow: 0 12px 16px 0 rgba(0,0,0,.24),
            0 17px 50px 0 rgba(0,0,0,.19);
        }

        #terminal {
            margin: 20px;
            padding: 14px;
            white-space: pre-wrap;
            word-break: break-all;
            font-size: 14px;
            overflow-y: auto;
            height: 94vh;
            box-sizing: border-box;
            /*background-color: rgba(102, 146, 191, 0.44);*/
            border: solid 1px rgba(102, 146, 191, 0.68);
            border-radius: 10px;
            -moz-box-shadow:2px 2px 5px #333333;
            -webkit-box-shadow:2px 2px 5px #333333;
            box-shadow: 7px 15px 30px #285a63;
        }

        #terminal div {
            margin-bottom: 7px;
        }

        /* 终端光标样式 */
        .terminal-cursor {
            display: inline !important;
            visibility: hidden;
            width: auto;
            height: auto;
            background: transparent;
            border-left: 2px solid #00ff00;
            animation: blink 1s step-end infinite;
        }

        @keyframes blink {
            from, to {
                opacity: 1;
            }
            50% {
                opacity: 0;
            }
        }

        .line {
            contain: content; /* 防止布局重排 */
        }

        /* 确保只有最后一行显示光标 */
        .line:last-child .terminal-cursor {
            visibility: visible !important;
        }

        /* 终端提示符样式 */
        .prompt {
            color: var(--prompt-color);
            user-select: none;
        }

        /* ANSI 颜色代码样式 */
        .ansi-bold { font-weight: bold; }
        .ansi-dim { opacity: 0.7; }
        .ansi-italic { font-style: italic; }
        .ansi-underline { text-decoration: underline; }

        /* 前景色 */
        .ansi-fg-black { color: #2e3436; }
        .ansi-fg-red { color: #cc0000; }
        .ansi-fg-green { color: #4e9a06; }
        .ansi-fg-yellow { color: #c4a000; }
        .ansi-fg-blue { color: #3465a4; }
        .ansi-fg-magenta { color: #75507b; }
        .ansi-fg-cyan { color: #06989a; }
        .ansi-fg-white { color: #d3d7cf; }
        .ansi-fg-bright-black { color: #555753; }
        .ansi-fg-bright-red { color: #ef2929; }
        .ansi-fg-bright-green { color: #8ae234; }
        .ansi-fg-bright-yellow { color: #fce94f; }
        .ansi-fg-bright-blue { color: #729fcf; }
        .ansi-fg-bright-magenta { color: #ad7fa8; }
        .ansi-fg-bright-cyan { color: #34e2e2; }
        .ansi-fg-bright-white { color: #eeeeec; }

        .ansi-fg-black { color: #444c4e; }
        .ansi-fg-red { color: #ff3333; }
        .ansi-fg-green { color: #66b300; }
        .ansi-fg-yellow { color: #e6c200; }
        .ansi-fg-blue { color: #4d85e4; }
        .ansi-fg-magenta { color: #95699c; }
        .ansi-fg-cyan { color: #29b7b9; }
        .ansi-fg-white { color: #e8e8e4; }

        .ansi-fg-black { color: #555753; }
        .ansi-fg-red { color: #ef2929; }
        .ansi-fg-green { color: #8ae234; }
        .ansi-fg-yellow { color: #fce94f; }
        .ansi-fg-blue { color: #729fcf; }
        .ansi-fg-magenta { color: #ad7fa8; }
        .ansi-fg-cyan { color: #34e2e2; }
        .ansi-fg-white { color: #eeeeec; }

        .ansi-fg-bright-black { color: #777a73; }
        .ansi-fg-bright-red { color: #ff5555; }
        .ansi-fg-bright-green { color: #aaff55; }
        .ansi-fg-bright-yellow { color: #ffff77; }
        .ansi-fg-bright-blue { color: #98bcf2; }
        .ansi-fg-bright-magenta { color: #c296bd; }
        .ansi-fg-bright-cyan { color: #56ffff; }
        .ansi-fg-bright-white { color: #ffffff; }


        /* 背景色 */
        .ansi-bg-black { background-color: #2e3436; }
        .ansi-bg-red { background-color: #cc0000; }
        .ansi-bg-green { background-color: #4e9a06; }
        .ansi-bg-yellow { background-color: #c4a000; }
        .ansi-bg-blue { background-color: #3465a4; }
        .ansi-bg-magenta { background-color: #75507b; }
        .ansi-bg-cyan { background-color: #06989a; }
        .ansi-bg-white { background-color: #d3d7cf; }
        .ansi-bg-bright-black { background-color: #555753; }
        .ansi-bg-bright-red { background-color: #ef2929; }
        .ansi-bg-bright-green { background-color: #8ae234; }
        .ansi-bg-bright-yellow { background-color: #fce94f; }
        .ansi-bg-bright-blue { background-color: #729fcf; }
        .ansi-bg-bright-magenta { background-color: #ad7fa8; }
        .ansi-bg-bright-cyan { background-color: #34e2e2; }
        .ansi-bg-bright-white { background-color: #eeeeec; }
    </style>
</head>
<body>

    <div class="terminal-controls">
        <button id="toggleRefresh" onclick="location.reload()">🔄 清屏</button>
        <button id="toggleScroll">⏸ 停止滚动</button>
        <button id="toggleUpdate">⏸ 停止更新</button>
    </div>

    <div id="terminal"></div>

    <script>

        const terminal = document.getElementById('terminal');

        // 清理ANSI代码并添加终端前缀
        function formatTerminalLine(text) {
            // 移除所有ANSI重置代码
            text = text.replace(/\033\[0m/g, '');

            // 解析其他ANSI颜色代码
            const ansiRegex = /\033\[([0-9;]+)m/g;
            let lastIndex = 0;
            let result = '';
            let match;

            while ((match = ansiRegex.exec(text)) !== null) {
                result += text.substring(lastIndex, match.index);
                lastIndex = ansiRegex.lastIndex;

                const codes = match[1].split(';');
                let classes = [];

                for (const code of codes) {
                    const num = parseInt(code, 10);
                    if (isNaN(num)) continue;

                    switch (num) {
                        case 1: classes.push('ansi-bold'); break;
                        case 2: classes.push('ansi-dim'); break;
                        case 3: classes.push('ansi-italic'); break;
                        case 4: classes.push('ansi-underline'); break;
                        case 30: classes.push('ansi-fg-black'); break;
                        case 31: classes.push('ansi-fg-red'); break;
                        case 32: classes.push('ansi-fg-green'); break;
                        case 33: classes.push('ansi-fg-yellow'); break;
                        case 34: classes.push('ansi-fg-blue'); break;
                        case 35: classes.push('ansi-fg-magenta'); break;
                        case 36: classes.push('ansi-fg-cyan'); break;
                        case 37: classes.push('ansi-fg-white'); break;
                        case 90: classes.push('ansi-fg-bright-black'); break;
                        case 91: classes.push('ansi-fg-bright-red'); break;
                        case 92: classes.push('ansi-fg-bright-green'); break;
                        case 93: classes.push('ansi-fg-bright-yellow'); break;
                        case 94: classes.push('ansi-fg-bright-blue'); break;
                        case 95: classes.push('ansi-fg-bright-magenta'); break;
                        case 96: classes.push('ansi-fg-bright-cyan'); break;
                        case 97: classes.push('ansi-fg-bright-white'); break;
                        case 40: classes.push('ansi-bg-black'); break;
                        case 41: classes.push('ansi-bg-red'); break;
                        case 42: classes.push('ansi-bg-green'); break;
                        case 43: classes.push('ansi-bg-yellow'); break;
                        case 44: classes.push('ansi-bg-blue'); break;
                        case 45: classes.push('ansi-bg-magenta'); break;
                        case 46: classes.push('ansi-bg-cyan'); break;
                        case 47: classes.push('ansi-bg-white'); break;
                    }
                }

                if (classes.length > 0) {
                    result += `<span class="${classes.join(' ')}">`;
                    const endTagIndex = text.indexOf('\033[', lastIndex);
                    if (endTagIndex !== -1) {
                        result += text.substring(lastIndex, endTagIndex);
                        lastIndex = endTagIndex;
                        result += '</span>';
                    }
                }
            }

            result += text.substring(lastIndex);

            // 添加终端前缀
            return addLinksToText(`<div><span class="prompt">(base)/root/wts&gt; </span>${result}</div>`);
        }

        function addLinksToText(text) {
            // 更宽松且能处理引号的 URL 匹配正则表达式
            const urlRegex = /(['"]?)(https?:\/\/(?:[a-zA-Z0-9.-]+|localhost|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?::\d+)?(?:\/[^\s]*)?)\1/g;

            return text.replace(urlRegex, function (match, quote, url) {
                return `<a href="${url}" target="_blank">${url}</a>`;
            });
        }

        // 全局变量
        let isTyping = false;
        const messageQueue = [];

        // 封装逐行逐字显示方法
        async function typeLineWithCursor(lineElement, formattedHtml) {
            const cursorDom = document.createElement('span');
            cursorDom.className = 'terminal-cursor';
            lineElement.appendChild(cursorDom); // 先添加到DOM
            // 1. 创建静态副本避免重绘
            const staticLine = lineElement.cloneNode();
            staticLine.innerHTML = formattedHtml;

            // 2. 获取文本节点和位置映射
            const textNodes = [];
            const nodePositions = [];
            let totalLength = 0;

            function scanNodes(node) {
                if (node.nodeType === Node.TEXT_NODE) {
                    textNodes.push(node);
                    nodePositions.push(totalLength);
                    totalLength += node.textContent.length;
                } else if (node.childNodes) {
                    Array.from(node.childNodes).forEach(scanNodes);
                }
            }

            scanNodes(staticLine);

            // 3. 逐字渲染（不再整体替换HTML）
            const cursor = document.createElement('span');
            cursor.className = 'terminal-cursor';
            lineElement.innerHTML = '';
            lineElement.appendChild(cursor);

            // 每次增加10个字符
            const BATCH_SIZE = 10;
            let pos = 0;

            while (pos <= totalLength) {
            //for (let pos = 0; pos <= totalLength; pos += 10) {
            //for (let pos = 0; pos < totalLength + 10; pos = Math.min(pos + 10, totalLength)) {
                const fragment = document.createDocumentFragment();
                let accumulatedPos = 0;

                textNodes.forEach((node, i) => {
                    const nodeStart = nodePositions[i];
                    const endPos = Math.min(pos + BATCH_SIZE, totalLength);
                    const visibleLength = Math.max(0, Math.min(endPos - nodeStart, node.textContent.length));

                    if (visibleLength > 0) {
                        const clone = node.parentNode.cloneNode(false);
                        clone.textContent = node.textContent.substring(0, visibleLength);
                        fragment.appendChild(clone);
                    }
                    accumulatedPos += node.textContent.length;
                });

                lineElement.replaceChildren(fragment, cursor);
                cursorDom.style.visibility = pos >= totalLength ? 'visible' : 'hidden';
                // cursor.style.display = pos === totalLength ? 'inline' : 'none';

                // 光标控制
                if (autoScroll) {
                    terminal.scrollTop = terminal.scrollHeight;
                    void terminal.offsetHeight;
                }

                // 保持适当间隔 1ms/10 字
                pos += BATCH_SIZE;
                await new Promise(r => setTimeout(r, 1));
            }
        }

        // 消息队列处理器
        async function processQueue() {
            if (isTyping || messageQueue.length === 0) return;

            isTyping = true;
            const {lineElement, formattedHtml} = messageQueue.shift();

            // 移除前一行光标
            const prevCursor = terminal.querySelector('.terminal-cursor');
            if (prevCursor) prevCursor.remove();

            await typeLineWithCursor(lineElement, formattedHtml);
            isTyping = false;

            // 继续处理下一行
            await processQueue();
        }

        const toggleScrollBtn = document.getElementById('toggleScroll');
        const toggleUpdateBtn = document.getElementById('toggleUpdate');

        // 状态控制变量
        let autoScroll = true;
        let pauseUpdates = false;
        let es = null;
        const MAX_LINES = 1000;  // 最大保留行数
        let lineCount = 0;
        const DISPLAY_TYPE = 1;                                   // 显示方式：1 - 逐字显示 | 0 整块显示

        // 心跳检测
        let lastHeartbeat = Date.now();
        const HEARTBEAT_TIMEOUT = 300000;          // 5分钟无心跳就断开
        const HEARTBEAT_CHECK_TIME = 60000;     // 每分钟检查一次心跳

        const checkHeartbeat = setInterval(() => {
            if (Date.now() - lastHeartbeat > HEARTBEAT_TIMEOUT) {
                es.close();
                toggleUpdate()
                // terminal.innerHTML += '<div class="error">连接超时已断开</div>';
                terminal.innerHTML += `<div><span class="prompt">(base)/root/wts&gt; </span>` +
                    `<span class="ansi-fg-red">连接超时已断开</span></div>`;
                clearInterval(checkHeartbeat);
            }
        }, HEARTBEAT_CHECK_TIME);

        // 初始化SSE连接
        function connectSSE() {
            es = new EventSource('/src/terminal/output?key=be9cb80c603519a7fdadc124e33954e0');

            es.onmessage = (e) => {
                if(pauseUpdates) return;
                if (e.data.startsWith(':')) {
                    lastHeartbeat = Date.now(); // 更新心跳时间
                    return;
                }

                // 内存控制：删除早期行
                if (lineCount >= MAX_LINES) {
                    const lines = terminal.querySelectorAll('.line');
                    if (lines.length > MAX_LINES * 0.8) { // 保留80%
                        terminal.removeChild(lines[0]);
                        lineCount--;
                    }
                }

                if (DISPLAY_TYPE === 1) {
                    // 逐字显示 - 加入队列 - 触发处理
                    const line = document.createElement('div');
                    line.className = 'line';
                    terminal.appendChild(line);
                    messageQueue.push({
                        lineElement: line,
                        formattedHtml: formatTerminalLine(e.data)
                    });
                    processQueue();
                } else {
                    // 整块显示
                    const line = document.createElement('div');
                    line.className = 'line';
                    line.innerHTML = formatTerminalLine(e.data);
                    terminal.appendChild(line);
                }

                lineCount++;
                if(autoScroll) terminal.scrollTop = terminal.scrollHeight;
            };

            es.onerror = () => {
                es.close();
                if(!pauseUpdates) setTimeout(connectSSE, 3000);
            };
        }

        // 初始连接
        connectSSE();

        // 滚动控制按钮
        toggleScrollBtn.addEventListener('click', () => {
              autoScroll = !autoScroll;
              toggleScrollBtn.textContent = autoScroll ? "⏸ 停止滚动" : "▶ 恢复滚动";
              if(autoScroll) terminal.scrollTop = terminal.scrollHeight;
        });

        // 更新控制按钮
        toggleUpdateBtn.addEventListener('click', () => {toggleUpdate()});

        function toggleUpdate() {
            pauseUpdates = !pauseUpdates;
            toggleUpdateBtn.textContent = pauseUpdates ? "▶ 恢复更新" : "⏸ 停止更新";

            if (!pauseUpdates && (!es || es.readyState === EventSource.CLOSED)) {
                connectSSE();
            } else if (pauseUpdates && es) {
                es.close();
            }
        }

    </script>
</body>
</html>
